Conversation
Add a new object storage provider plugin for SeaweedFS, alongside the
existing MinIO, Ceph RGW, and Cloudian HyperStore providers.
SeaweedFS exposes an S3-compatible API and an AWS IAM-compatible API, so
this provider uses the AWS S3 and IAM Java SDKs — the same approach as
the Cloudian HyperStore provider. No proprietary admin client is needed.
Key features:
- Bucket CRUD, policy, versioning, encryption, ACLs via AmazonS3 SDK
- Per-account IAM user provisioning via AmazonIdentityManagement SDK
- Per-bucket quota via the SeaweedFS S3 ?seaweedfs-quota extension
(PUT /{bucket}?seaweedfs-quota), authenticated via SigV4 and authorized
via the s3:PutBucketQuota IAM permission. This requires SeaweedFS PR
apache#11279.
- Usage reporting via S3 ListObjectsV2 (MVP; Prometheus or SOSAPI
capacity.xml recommended for production scale)
The service credential (accesskey/secretkey on the object store) is
granted only s3:PutBucketQuota and s3:GetBucketQuota via an IAM policy,
so it cannot delete buckets, manage users, or change cluster topology.
The plugin follows the Cloudian HyperStore pattern almost line for line:
same store-details keys (s3Url, iamUrl, accesskey, secretkey), same
IAM-user-with-restricted-policy pattern, same Spring wiring.
SeaweedFS registers its embedded IAM API at POST / on the same S3 endpoint (UnifiedPostHandler in s3api_server.go), not under /iam. The AWS IAM SDK uses the Query protocol and POSTs to the endpoint root, so defaulting iamUrl to <s3Url>/iam would send IAM operations to an unregistered path. Default to s3Url instead; a separate iamUrl is only needed for deployments running a standalone weed iam server. Found by Greptile review on PR apache#11279.
…licy constant Two issues found by CodeRabbit review on PR apache#11279: 1. S3Signer implements legacy S3 Signature Version 2, not SigV4. SeaweedFS expects SigV4. Replace with AWSS3V4Signer which implements AWS Signature Version 4. The seaweedfs-quota query parameter is included in the signed canonical query string. 2. SERVICE_CREDENTIAL_POLICY was a dead constant (never referenced) that claimed the service credential is scoped to only s3:PutBucketQuota/s3:GetBucketQuota. This contradicts the actual implementation, which uses the service credential (admin) for all driver operations: bucket CRUD, IAM user provisioning, and quota. Remove the dead constant and document the actual credential model.
|
Congratulations on your first Pull Request and welcome to the Apache CloudStack community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md)
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved packaging, lifecycle, IAM isolation, credential handling, and quota-signing issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds SeaweedFS as a CloudStack object-storage provider using S3/IAM APIs and SeaweedFS quota support.
Changes:
- Implements provider lifecycle, bucket, IAM, quota, and usage operations.
- Adds Maven and Spring module registration.
- Adds provider and driver tests.
File summaries
| File | Description |
|---|---|
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/provider/SeaweedFSObjectStoreProviderImplTest.java |
Tests provider registration. |
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java |
Tests driver behavior and quota handling. |
plugins/storage/object/seaweedfs/src/main/resources/META-INF/cloudstack/storage-object-seaweedfs/spring-storage-object-seaweedfs-context.xml |
Registers Spring provider wiring. |
plugins/storage/object/seaweedfs/src/main/resources/META-INF/cloudstack/storage-object-seaweedfs/module.properties |
Defines module metadata. |
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java |
Builds clients and quota requests. |
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/provider/SeaweedFSObjectStoreProviderImpl.java |
Registers the provider. |
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SeaweedFSObjectStoreLifeCycleImpl.java |
Handles pool lifecycle and validation. |
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java |
Implements storage and IAM operations. |
plugins/storage/object/seaweedfs/pom.xml |
Defines module dependencies. |
plugins/pom.xml |
Registers the SeaweedFS module. |
Review details
Suppressed comments (4)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:430
- On any transient per-bucket S3 failure, this inserts
0.BucketApiServiceImpltreats returned values as authoritative and persists bucket and object-store usage, so a temporary timeout resets usage to zero and under-reports capacity. Propagate the failure for the whole store (or skip the store update) instead of publishing zero.
} catch (AmazonClientException e) {
logger.warn("Failed to get usage for bucket {}: {}", bucket.getName(), e.getMessage());
bucketUsage.put(bucket.getName(), 0L);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:271
HttpClient.newHttpClient()and this request have no connect or request timeout. A stalled SeaweedFS endpoint can block the synchronous bucket create/update API worker indefinitely. Use a shared client with configured connect and request timeouts, preferably using the provider's existing timeout configuration.
java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
java.net.http.HttpResponse<String> response = client.send(reqBuilder.build(),
java.net.http.HttpResponse.BodyHandlers.ofString());
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:261
- The request adds
Content-Lengthto the SDK headers at line 243, then copies every header intojava.net.http.HttpRequest.Builder. Java's HttpClient rejectsContent-Lengthas a restricted header, so this loop can throw before the request is sent. Filter transport-managed headers (at leastContent-Length, andHostif present) and let HttpClient generate them while signing the same body.
for (java.util.Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
reqBuilder.header(entry.getKey(), entry.getValue());
}
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java:280
- The common
setUpalready populatesstoreDetailsMapwith the S3 URL and both credentials, so this test never exercises the missing-configuration guard; it passes only because the real HTTP call fails. Clear or override the details map and assert the validation message before any network call.
// No S3 URL/credentials configured — should throw with a clear message
assertThrows(CloudRuntimeException.class, () -> driver.setBucketQuota(bucketTO, TEST_STORE_ID, 10));
- Files reviewed: 10/10 changed files
- Comments generated: 6
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| <module>storage/object/minio</module> | ||
| <module>storage/object/ceph</module> | ||
| <module>storage/object/cloudian</module> | ||
| <module>storage/object/seaweedfs</module> |
- ship provider in client packaging (add cloud-plugin-storage-object-seaweedfs to client/pom.xml), matching the other object-storage providers - copy the size param through initialize() so ObjectStoreHelper no longer NPEs on addObjectStoragePool - sign ?seaweedfs-quota as a canonical query parameter (split path/query, addParameter before signing) instead of leaving it unsigned in the URI - skip restricted HTTP headers (Content-Length/Host/...) when copying signed headers onto the java.net.http request - make the S3-extension HttpClient injectable so quota tests assert the signed path/headers/body without hitting the network - createUser now reuses a stored IAM access key when it still exists in IAM, only creating a replacement (after cleaning up unmanaged leftover keys) when the stored key is gone, preventing credential rotation and IAM access-key limits - rewrite quota tests to assert the signed request via a mock HttpClient; add createUser reuse and replacement tests
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate and critical issues remain in quota handling, credential management, usage reporting, endpoint updates, and concurrency.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (10)
client/pom.xml:669
- The existing Add Object Storage UI hard-codes its provider list and has no
SeaweedFSentry (ui/src/views/infra/AddObjectStorage.vue:130), so adding this dependency still leaves the provider unavailable through the supported UI. Add the provider to the UI (the default URL fields already match this lifecycle) or explicitly limit the feature to API registration.
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-storage-object-seaweedfs</artifactId>
<version>${project.version}</version>
</dependency>
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:520
initializealways persists a resolveds3Urlin the object-store details, so this branch will return that stale value afterupdateObjectStorechangesObjectStoreVO.url. The generic update path then callslistBuckets()against the old endpoint and can accept an unreachable new URL; subsequent bucket operations continue using the old endpoint. Synchronize the detail URL when the store URL changes or use the current store URL for this provider.
Map<String, String> storeDetails = _storeDetailsDao.getDetails(storeId);
String s3Url = storeDetails.get(SeaweedFSObjectStoreUtil.STORE_DETAILS_KEY_S3_URL);
if (s3Url == null || s3Url.isEmpty()) {
ObjectStoreVO store = _storeDao.findById(storeId);
s3Url = store.getUrl();
}
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:170
- When the stored key is missing, this path creates a replacement and persists it only in account details. Existing
BucketVOrows still contain the old pair;createBucketResponseexposes those row values, so previously created buckets continue to hand clients invalid credentials after rotation. Update all buckets for this store/account with the new key pair, as the Cloudian driver does.
// Persist the credentials in the account details
details.put(SeaweedFSObjectStoreUtil.KEY_ACCESS_KEY, key.getAccessKeyId());
details.put(SeaweedFSObjectStoreUtil.KEY_SECRET_KEY, key.getSecretAccessKey());
_accountDetailsDao.persist(accountId, details);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:500
ListObjectsV2reports only current versions. For a versioned bucket, noncurrent object versions are omitted even though they still consume storage, so usage andBucketVO.sizeare undercounted after overwrites or deletes. Use a version-aware listing or a SeaweedFS usage endpoint when versioning is enabled.
for (com.amazonaws.services.s3.model.S3ObjectSummary summary : result.getObjectSummaries()) {
size += summary.getSize();
}
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:458
- CloudStack's createBucket API makes quota required and calls
setQuotaeven for a value of 0, so a SeaweedFS deployment without this extension receives a 404 here and the surrounding create flow rolls the bucket back. That contradicts the description that bucket CRUD still works without the extension; either make zero quota a no-op/handle unsupported servers, or make the extension an explicit hard prerequisite for bucket creation.
SeaweedFSObjectStoreUtil.setBucketQuotaViaS3Extension(s3Url, accessKey, secretKey, bucket.getName(), size, getS3ExtensionHttpClient());
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:164
- Two concurrent
createUsercalls for the same account can both observe no stored key and no IAM keys, then each create and persist a different key. The last persistence wins, leaving the other key unmanaged; later calls see the persisted key and never enterdeleteUnmanagedAccessKeys, so the orphan can accumulate and eventually hit IAM key limits. Serialize per-account provisioning or re-list and reconcile keys after creation.
CreateAccessKeyResult result = iamClient.createAccessKey(
new CreateAccessKeyRequest().withUserName(userName));
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:74
s3:*onResource: "*"is not a restricted per-account policy. The credentials persisted into each account's bucket record/response can read and write every bucket and, with SeaweedFS's news3:PutBucketQuotaaction, change any tenant's quota; the deny only covers bucket creation/deletion. Scope account credentials to that account's buckets/object actions and keep quota/admin mutations on a separate service credential.
" \"s3:*\"\n" +
" ],\n" +
" \"Resource\": \"*\"\n" +
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:328
- Only status codes
>= 400are treated as failures, so a3xxresponse is reported as a successful quota update even thoughHttpClient.newHttpClient()does not follow redirects by default and the mutation was not applied. Accept only the 2xx success range here.
if (response.statusCode() >= 400) {
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:58
AccountDetailsDaois account-scoped—its lookup and persistence APIs accept onlyaccountId—while this driver provisions credentials for a specificstoreId. If one account uses two SeaweedFS pools, the second provisioning overwrites these fixed keys; a later bucket creation in the first pool then reads and publishes the second pool's credentials. Namespace the keys by store ID or use store-scoped credential storage.
public static final String KEY_ACCESS_KEY = "swfs_AccessKey";
public static final String KEY_SECRET_KEY = "swfs_SecretKey";
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java:345
setUp()always stubsgetDetails(TEST_STORE_ID)with a valid URL and credentials, so this test does not exercise the missing-configuration path. It instead creates a realHttpClientand attempts a network request, allowing the test to pass for the wrong reason. Clear or overridestoreDetailsMapbefore the assertion so the exception is caused by missing credentials.
public void testSetBucketQuotaNoS3ConfigThrows() {
BucketTO bucketTO = mock(BucketTO.class);
when(bucketTO.getName()).thenReturn(TEST_BUCKET_NAME);
// No S3 URL/credentials configured — should throw with a clear message
assertThrows(CloudRuntimeException.class, () -> driver.setBucketQuota(bucketTO, TEST_STORE_ID, 10));
- Files reviewed: 11/11 changed files
- Comments generated: 4
- Review effort level: Lite
AccountDetailsDao is account-scoped, so fixed key names like swfs_AccessKey meant a second SeaweedFS pool for the same account would overwrite the first pool's credentials. Replace the fixed constants with keyAccessKey(storeId)/keySecretKey(storeId) methods that namespace by store ID.
When createUser creates a replacement IAM access key, existing BucketVO rows still carried the old key pair, so previously created buckets kept handing clients invalid credentials. Add updateAccountBucketCredentials to update all bucket records for the store/account, mirroring the Cloudian HyperStore driver.
…etail initialize() persists a resolved s3Url in the object-store details, so getS3Url returned that stale value after updateObjectStore changed ObjectStoreVO.url. Bucket operations continued using the old endpoint. Prefer the current store URL and fall back to the detail only if it is missing.
Returning 0 for a failed S3 listing caused BucketApiServiceImpl to overwrite the stored BucketVO.size with a false zero, erasing known usage on a transient endpoint or permission failure. Omit the bucket from the result map instead so the caller retains the previous value.
HttpClient.newHttpClient() had no connect timeout and the HttpRequest had no per-request timeout, so a stalled or unreachable SeaweedFS endpoint could block the synchronous bucket create/update API indefinitely. Add a 10s connect timeout on the client and a 30s request timeout on each HttpRequest.
Only status codes >= 400 were treated as failures, so a 3xx response was reported as a successful quota update even though HttpClient does not follow redirects by default and the mutation was not applied. Accept only the 2xx range. Add a test asserting 3xx is rejected.
…list The Add Object Storage view hard-codes its provider list and had no SeaweedFS entry, so the provider was only available via API. Add it to the dropdown; the default URL/accessKey/secretKey fields already match this lifecycle.
…e details setUp() always stubs getDetails with a valid URL and credentials, so the test did not exercise the missing-configuration path — it created a real HttpClient and attempted a network request, passing for the wrong reason. Clear storeDetailsMap and null the store lookup so the exception comes from the missing-config check.
The quota tests only checked that an Authorization header exists; they did not verify the SigV4 canonical query, payload hash, or signed headers against a known signature. A signing mismatch would therefore pass the suite and make every quota operation fail against SeaweedFS. Add a test that independently signs the same request through AWSS3V4Signer and asserts the Authorization, x-amz-content-sha256, and x-amz-date headers match exactly.
bc00f66 to
e353f6f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Ten unresolved moderate findings affect credential consistency, endpoint handling, tenant isolation, quota validation, and test determinism.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (8)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:172
- Persisting the new key before updating BucketVOs creates a one-way inconsistency: if
updateAccountBucketCredentialsfails for any existing bucket, the key remains stored while some bucket rows still contain old credentials. The next invocation returns at lines 155-157 without retrying the repair, so those buckets can continue exposing invalid credentials. Make persistence and bucket updates transactional, or reconcile all bucket rows on the reuse path.
_accountDetailsDao.persist(accountId, details);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:554
- When
iamUrlis omitted,initialize()stores it as the initials3Url, butupdateObjectStoreonly changesObjectStoreVO.url. After a URL update, this accessor keeps IAM provisioning on the old endpoint while S3 operations use the new one, so account or bucket creation can fail or modify the wrong SeaweedFS instance. Track whetheriamUrlwas explicit or update this detail with the URL change.
protected String getIAMUrl(long storeId) {
Map<String, String> storeDetails = _storeDetailsDao.getDetails(storeId);
return storeDetails.get(SeaweedFSObjectStoreUtil.STORE_DETAILS_KEY_IAM_URL);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:207
iamAccessKeyExistsonly matches the key ID, so an IAM key whose status isInactiveis treated as usable. If an administrator disables the stored key,createUserreturns true and new bucket records continue using credentials that cannot authenticate. CheckAccessKeyMetadata.getStatus()and rotate/replace inactive keys before reusing them.
if (accessKeyId.equals(metadata.getAccessKeyId())) {
return true;
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:157
- The reuse check validates only the access-key ID. If the secret-key detail is missing or corrupted, this returns success and never repairs the credential pair, leaving newly created or existing bucket records unusable. Require both stored values before reusing the key; otherwise treat the pair as invalid and rotate it without preserving the unusable key.
String storedAccessKeyId = details.get(accessKeyDetailKey);
if (storedAccessKeyId != null && iamAccessKeyExists(iamClient, userName, storedAccessKeyId)) {
logger.debug("Reusing existing IAM access key {} for user {}", storedAccessKeyId, userName);
return true;
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:213
- This catch treats every
AmazonClientExceptionfromListAccessKeysas evidence that the stored key is absent. A timeout, authentication failure, or temporary IAM outage therefore sendscreateUserinto the replacement path, which can overwrite the persisted credentials, invalidate bucket records, or hit the IAM key limit. Propagate or retry lookup failures and rotate only after a successful listing that omits the stored ID.
} catch (AmazonClientException e) {
logger.warn("Failed to list IAM access keys for user {}: {}", userName, e.getMessage());
}
return false;
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:99
- These IAM credentials are persisted for the account and exposed as bucket credentials, so this policy is the tenant boundary. Allowing
s3:*onResource: "*"gives every account access to every bucket in the SeaweedFS pool (includingPutBucketQuota); denying only create/delete does not isolate accounts. Scope the policy to the account's bucket ARNs and update it as buckets are created or deleted, as the MinIO driver does.
" \"Action\": [\n" +
" \"s3:*\"\n" +
" ],\n" +
" \"Resource\": \"*\"\n" +
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:341
pathalways starts with/, soURI.resolve(path)replaces any path prefix ins3Url. For an endpoint behind a reverse proxy such ashttps://host/object-s3, the outgoing quota request is sent to/bucketinstead of/object-s3/bucket, so it reaches the wrong route (and can fail signature verification). Build the outgoing URI while preserving the endpoint's existing path.
java.net.URI fullUri = endpointUri.resolve(path);
if (! queryString.isEmpty()) {
fullUri = java.net.URI.create(fullUri.toString() + "?" + queryString);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:251
- Negative quotas are treated as a disable request here, but
updateBucketQuotapersists the caller's negative value and computes resource accounting from it. Changing a 5-GiB bucket to -1 therefore frees 6 GiB and stores -1 even though the server quota was cleared. Reject negative values; only zero should disable.
if (sizeGiB <= 0) {
- Files reviewed: 12/12 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved moderate issues affect bucket consistency, API responses, and usage accuracy.
Review details
Suppressed comments (5)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:557
- The lock is released here before
BucketApiServiceImplremoves theBucketVO. A concurrentcreateUser/createBucketcan acquire the lock after this block, observe the still-present row, and publish a policy that re-adds this bucket; the row is then removed, leaving a stale ARN grant. Since bucket names are reusable, the old account could access a later account's bucket with the same name. Keep row removal inside the same lock scope or otherwise serialize deletion with policy refresh.
} finally {
iamLock.unlock();
iamLock.releaseRef();
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:439
- Although this returns the updated
BucketVO,BucketApiServiceImpl.createBucketdiscards that return value and ultimately returns its originalbuckettoCreateBucketCmd. The response generator therefore sees null or staleaccessKey,secretKey, andbucketURLimmediately after a successful create, even though these fields are populated in the database here and are part ofBucketResponse. Propagate the returnedBucketVOinto the API response/state update.
// Return the updated BucketVO (not the stale input bucket) so
// BucketApiServiceImpl.createBucket does not overwrite the
// persisted credentials with the stale values.
return bucketVO;
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:774
- SeaweedFS refreshes
bucket_size_bytesonly on the S3 instance holding its distributeds3.leaderlock. IfmetricsUrlpoints to a load-balanced service, this request can hit a non-leader with stale or empty gauges; the parser then accepts those values and bypasses the S3 fallback, so CloudStack can under-report usage. Use a stable leader endpoint or an aggregated/freshness-checked metrics source.
try {
return SeaweedFSObjectStoreUtil.parseBucketUsageFromMetrics(
metricsUrl, bucketNames, getS3ExtensionHttpClient());
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:563
- This check also matches Prometheus
# HELPand# TYPEmetadata lines, so a response with no bucket samples is accepted. The result was prefilled with zeroes and is then returned without the S3 fallback, causing every managed bucket to be reported as 0 bytes during an exporter response that only contains the metric declaration. Require an actual sample line (or otherwise treat a sample-less family as a scrape failure).
if (!body.contains(METRIC_BUCKET_SIZE_BYTES)) {
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:547
- Appending
/metricsdirectly makes a configured URL ending in/request//metrics. That can return a redirect or 404;HttpClientdoes not follow redirects here, so the driver silently falls back to the O(total objects) S3 scan on every usage poll and loses the scalability benefit ofmetricsUrl. Normalize trailing slashes before appending the endpoint path.
java.net.URI uri = java.net.URI.create(metricsUrl + "/metrics");
- Files reviewed: 16/16 changed files
- Comments generated: 0 new
- Review effort level: Lite
Three ways the metrics path could bypass the S3 fallback and report zero usage for every managed bucket: 1. A metricsUrl ending in '/' produced a '//metrics' request, which can redirect or 404. HttpClient does not follow redirects here, so the driver silently fell back to the O(total objects) S3 scan on every usage poll, losing the scalability benefit. Trailing slashes are now normalized before appending the endpoint path. 2. The metric-family check matched Prometheus '# HELP' and '# TYPE' metadata lines, so a response declaring the family but exporting no samples was accepted and the prefilled zeroes were returned. Comment lines are now skipped. 3. SeaweedFS refreshes the bucket-size gauges only on the S3 instance holding the distributed s3.leader lock, so a load-balanced metricsUrl can hit a non-leader with empty gauges. Since SeaweedFS publishes a zero gauge for empty buckets, the leader always exports one sample per bucket it knows about; the parser now requires a sample for every managed bucket and raises a scrape failure otherwise, so the caller falls back to the accurate S3 listing.
The IAM lock was released before BucketApiServiceImpl removed the BucketVO row. A concurrent createUser/createBucket could acquire the lock in that window, observe the still-present row, and publish a policy that re-added the deleted bucket ARN; the row was then removed, leaving a stale grant. Since bucket names are reusable, the old account could access a later account's bucket with the same name. deleteBucket now removes the row itself, inside the lock and after the policy refresh succeeds. A policy failure still propagates with the row intact so the operation stays retryable and BucketApiServiceImpl leaves its accounting alone. On success BucketApiServiceImpl's own _bucketDao.remove is a no-op while its resource-limit and allocated-size cleanup still runs.
createBucket captured the driver's return value only as a BucketTO and then wrote the original in-memory bucket back with _bucketDao.update, clobbering the access key, secret key, and bucket URL that the provider had just persisted. The stale object was also returned to CreateBucketCmd, so BucketResponse exposed null credentials immediately after a successful create even though the database held the real values. The row is now re-read before the state update so provider-persisted fields survive and reach the API response. Providers such as SeaweedFS and Cloudian HyperStore write per-account credentials to the BucketVO themselves.
… removal Four new tests: - testDeleteBucketRemovesRowInsideLock: the driver removes the BucketVO itself so no concurrent policy rebuild can observe a stale row. - testGetAllBucketsUsageMetadataOnlyFallsBackToList: a HELP/TYPE-only response (non-leader S3 instance) triggers the S3 fallback. - testGetAllBucketsUsageMissingSampleFallsBackToList: a response missing a sample for one managed bucket triggers the S3 fallback. - testGetAllBucketsUsageMetricsUrlTrailingSlashNormalized: a metricsUrl with a trailing slash requests /metrics, not //metrics.
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved moderate issues affect deletion accounting, endpoint consistency, metrics preservation, and credential validation.
Review details
Suppressed comments (4)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:568
- This removes the
BucketVObeforeBucketApiServiceImpl.deleteCheckedBucketupdates resource counts and the object-store allocated size. If either later cleanup operation fails or returns false, the remote bucket and IAM grant have already been removed while CloudStack still accounts for the bucket/quota, and a retry cannot find the row to reconcile the state. Keep row deletion coupled to successful accounting, or coordinate the lock and caller cleanup so the row is not removed first.
for (BucketVO bvo : _bucketDao.listByObjectStoreIdAndAccountId(storeId, accountId)) {
if (bucketName.equals(bvo.getName())) {
_bucketDao.remove(bvo.getId());
break;
}
}
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SeaweedFSObjectStoreLifeCycleImpl.java:96
- Leaving the default endpoints out of the details makes
getS3Url()follow a laterupdateObjectStoreURL change, butBucketVO.bucketURLis written only during bucket creation.BucketResponseexposes that stored URL and the object-store browser uses it to construct its S3 client, so after changing the pool URL CloudStack operations use the new endpoint while the browser still targets the old one. Please update existing bucket URLs when the pool URL changes, or avoid relying on this fallback without keeping those values consistent.
// driver falls back to ObjectStoreVO.url / getS3Url() when the
// details are absent, so updateObjectStore can change the store URL
// without a stale persisted s3Url/iamUrl overriding it.
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SeaweedFSObjectStoreLifeCycleImpl.java:90
metricsUrlis accepted by the UI and sent in the API details map, but this initializer never reads or preserves it. ConsequentlyObjectStoreHelper.createObjectStoredrops the configured metrics endpoint,getMetricsUrl()always returnsnull, and usage reporting always falls back to the O(total objects)ListObjectsV2scan instead of using the advertised scalable path. Retain this detail (and remove it when omitted) alongside the S3/IAM endpoint overrides.
String s3Url = details.get(SeaweedFSObjectStoreUtil.STORE_DETAILS_KEY_S3_URL);
String iamUrl = details.get(SeaweedFSObjectStoreUtil.STORE_DETAILS_KEY_IAM_URL);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SeaweedFSObjectStoreLifeCycleImpl.java:138
- These validation calls use the literal
unknowncredentials rather than the configuredaccessKey/secretKey(seevalidateS3Url/validateIAMUrl), so an object store with invalid admin credentials is accepted and only fails on the first bucket or IAM operation. Authenticate a validation request with the supplied credentials (or otherwise verify them) before persisting the pool, as the other providers do during initialization.
SeaweedFSObjectStoreUtil.validateS3Url(s3Url);
logger.info("Validating SeaweedFS IAM endpoint: {}", iamUrl);
SeaweedFSObjectStoreUtil.validateIAMUrl(iamUrl);
- Files reviewed: 16/16 changed files
- Comments generated: 0 new
- Review effort level: Lite
…deleteBucket Removing the BucketVO inside the IAM lock closed the stale-grant window but broke deletion accounting: BucketApiServiceImpl.deleteCheckedBucket decrements the resource counts and allocated size after the driver returns, so if any of that failed the row was already gone and a retry could not find it to reconcile, while CloudStack still accounted for the bucket and quota. deleteBucket now marks the row Bucket.State.Destroyed inside the lock and buildAccountIAMPolicy skips Destroyed rows. The row survives for the caller's accounting and for reconciliation on failure, while a concurrent createUser/createBucket that rebuilds the policy from the DB can no longer re-add the deleted bucket's ARN on a name that is now reusable.
…ation The UI accepts metricsUrl and the API passes it in the details map, but initialize() never read it, so ObjectStoreHelper.createObjectStore dropped it. getMetricsUrl() therefore always returned null and usage reporting always fell back to the O(total objects) ListObjectsV2 scan, meaning the scalable metrics path could never actually be used. The detail is now retained when supplied and removed when omitted, alongside the S3 and IAM endpoint overrides.
…lization validateS3Url and validateIAMUrl deliberately connect with the literal 'unknown' credentials to probe that the endpoint behaves like the respective service, so on their own they accept an object store whose admin access key or secret key is wrong; the failure only surfaced later on the first bucket or IAM operation. Added validateCredentials, which performs an authenticated listBuckets and listUsers with the supplied credentials, and called it from initialize() so a bad-credential store is rejected at registration time as the other providers do.
BucketVO.bucketURL is written only at bucket creation. BucketResponse exposes it and the object-store browser builds its S3 client from it, so after updateObjectStore changed the pool URL the management server used the new endpoint while the browser kept targeting the old one. updateObjectStore now rewrites the bucketURL of every bucket on the store, replacing the old URL prefix with the new one, once the new URL has been verified accessible.
…nd credential validation - testDeleteBucketMarksRowDestroyedInsideLock: the row is marked Destroyed inside the IAM lock and NOT removed, so the caller's accounting still has it. - testBuildPolicySkipsDestroyedBuckets: a Destroyed bucket's ARN is left out of the account policy. - testInitializeMetricsUrlPersisted / ...OmittedNotPersisted: the metrics endpoint survives initialization when supplied and is not persisted when omitted. - testInitializeValidatesSuppliedCredentials: initialization authenticates with the configured admin credentials.
There was a problem hiding this comment.
🟡 Changes recommended
A critical bucket-isolation race and additional credential, quota-accounting, and endpoint-update issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:232
AccountDetailsDao.persistexpunges every existing detail for the account before inserting this map (engine/schema/src/main/java/com/cloud/user/AccountDetailsDaoImpl.java:74-85). This lock is keyed bystoreId + accountId, so two pools provisioning the same CloudStack account concurrently can each read a snapshot and the later persist can delete the other store's namespaced credentials, breaking existing buckets. Serialize account-wide account-detail writes or replace this with an atomic per-key update.
_accountDetailsDao.persist(accountId, details);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:743
- This remote quota mutation is not atomic with CloudStack's resource accounting:
BucketApiServiceImpl.updateBucketQuotacallssetQuotabefore constructing itsCheckedReservation. If an increase exceeds the account/store limit, the reservation fails after SeaweedFS has applied the new quota, while the BucketVO remains unchanged. Reserve first or add a compensating quota update on failure so the backend and accounting cannot diverge.
boolean allowMissingExtension = !hasPositiveQuota(storeId, bucket);
SeaweedFSObjectStoreUtil.setBucketQuotaViaS3Extension(s3Url, accessKey, secretKey, bucket.getName(), size,
getS3ExtensionHttpClient(), allowMissingExtension);
server/src/main/java/com/cloud/storage/StorageManagerImpl.java:4802
- This rewrites bucket URLs for every provider even when the pool has an explicit
s3Url. For example, a SeaweedFS/Cloudians3Urlunder the old generic URL is rewritten when onlyObjectStoreVO.urlchanges, although the driver continues using the explicit endpoint; the browser then targets a different, invalid endpoint. Skip this rewrite whens3Urlis configured, or rewrite based on the effective S3 endpoint rather than the generic store URL.
updateBucketUrls(id, oldUrl, url);
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Lite
… other pools AccountDetailsDao.persist(accountId, map) expunges every existing detail for the account before inserting the supplied map, making it a read-modify-write of the whole account detail set. The IAM lock is keyed by storeId+accountId, so two object stores provisioning the same CloudStack account concurrently could each read a snapshot and have the later persist delete the other store's namespaced credentials, breaking every bucket that referenced them. It would also drop unrelated account details. createUser now writes each namespaced credential with AccountDetailsDao.addDetail, which only touches the named key, so concurrent provisioning of distinct stores cannot interfere. Tests assert the per-key writes and that the account-wide persist is no longer used.
updateBucketQuota applied the backend quota before constructing its CheckedReservation, so an increase that exceeded the account or object store limit left the storage backend with the new quota while the BucketVO and resource counts kept the old value. The reservation is now taken first and the remote setQuota happens inside it, so a limit failure makes no backend change. A compensating setQuota restores the previous value if the resource-count or allocated-size update fails after the remote call, on both the increase and decrease paths, and bucket.setQuota is only applied once accounting has succeeded. Also defaults a null existing quota to 0 instead of dereferencing it.
updateBucketUrls rewrote stored bucket URLs for every provider whenever ObjectStoreVO.url changed. Providers that support an explicit s3Url detail (SeaweedFS, Cloudian HyperStore) keep using that endpoint regardless of the generic URL, so rewriting off the generic URL pointed the object store browser at an endpoint the driver never uses. The rewrite is now skipped when an s3Url detail is configured.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate correctness, accounting, URL, and authorization-safety issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:750
hasPositiveQuotacannot distinguish initial creation from an update whose previous quota is zero. If an update from 0 to a positive quota succeeds remotely and the accounting update then fails,restoreRemoteQuota(..., previousQuota = 0)reaches this branch; a 404/405 during rollback is silently treated as a missing optional extension, even though the positive update already proved the extension was available. CloudStack can then retain quota 0 while SeaweedFS remains quota-limited/read-only. Only tolerate a missing extension for the initial create; rollback and updates must propagate the failure.
boolean allowMissingExtension = !hasPositiveQuota(storeId, bucket);
SeaweedFSObjectStoreUtil.setBucketQuotaViaS3Extension(s3Url, accessKey, secretKey, bucket.getName(), size,
getS3ExtensionHttpClient(), allowMissingExtension);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:441
- Because
s3Urlis user-configurable and not normalized, an otherwise valid endpoint ending in/produces...//buckethere. That value is persisted inBucketVO.bucketURLand used by the browser, so such registrations create broken bucket URLs. Strip trailing slashes before appending the bucket path.
bucketVO.setBucketURL(s3Url + "/" + bucketName);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:577
- This lock is scoped to
(storeId, accountId), so it does not serialize deletion here withcreateBucketfor a different account. Once the S3 delete succeeds but before this policy refresh completes, another account can recreate the globally unique bucket name while the old account's policy still grants that ARN, allowing the old credentials to access the new tenant's bucket. Use a store-wide bucket-name lock shared by create/delete, or revoke the old grant before the name can be reused.
AmazonIdentityManagement iamClient = getIAMClient(storeId);
updateAccountIAMPolicyLocked(iamClient, storeId, accountId, bucketName);
server/src/main/java/com/cloud/storage/StorageManagerImpl.java:4851
newUrlis accepted with a trailing slash, while the stored suffix normally starts with/; this concatenation can rewrite every bucket URL to...//bucket, breaking browser access after a pool URL update. Normalize the base URL before concatenating.
bucket.setBucketURL(newUrl + bucketUrl.substring(oldUrl.length()));
server/src/main/java/org/apache/cloudstack/storage/object/BucketApiServiceImpl.java:368
- This compensation only restores the remote quota, not CloudStack's accounting.
incrementResourceCountcan complete beforeupdateAllocatedSizethrows, andCheckedReservation.close()only removes the reservation; moreoverupdateAllocatedSizereturnsfalseon an unsuccessful DAO update instead of throwing, so the catch is skipped entirely. In either case the method can leave the resource count/allocated size inconsistent with the restored backend quota; check the Boolean result and explicitly compensate all accounting mutations before returning the error.
resourceLimitManager.incrementResourceCount(bucket.getAccountId(), Resource.ResourceType.object_storage, diff);
_objectStoreDao.updateAllocatedSize(objectStoreVO, diff);
} catch (RuntimeException e) {
restoreRemoteQuota(objectStore, bucketTO, bucket.getName(), previousQuota, e);
server/src/main/java/org/apache/cloudstack/storage/object/BucketApiServiceImpl.java:371
- This assignment completes the remote/accounting side of the quota change, but the enclosing method persists
BucketVOonly afterward. If that later_bucketDao.updatefails or returns false, the API can report an error or success while the remote quota, resource counters, andBucketVOdisagree; add an outer compensation path or make the state/accounting update transactional with this change.
bucket.setQuota(quota);
- Files reviewed: 17/17 changed files
- Comments generated: 3
- Review effort level: Lite
| // account rebuilds the policy from the DB and re-adds this bucket's | ||
| // ARN: buildAccountIAMPolicy skips Destroyed rows, so the grant | ||
| // cannot come back on a name that is now reusable. | ||
| markBucketDestroyed(storeId, accountId, bucketName); |
There was a problem hiding this comment.
Fixed in 932cc3c. The driver still marks the bucket row Destroyed, but BucketApiServiceImpl.deleteCheckedBucket now performs the resource-count updates, allocated-size update, and row removal together in one transaction after the driver returns, checking the DAO results. That keeps retry paths from double-decrementing object-store accounting. Added a regression test for allocated-size failure.
…d delete The IAM lock is scoped to (storeId, accountId), so it did not serialize a deletion against createBucket for a *different* account. S3 bucket names are globally unique per store and reusable, so once the S3 delete succeeded but before the old owner's IAM policy refresh completed, another account could recreate the name while the old owner's policy still granted that ARN, letting the old credentials reach the new tenant's bucket. createBucket and deleteBucket now both take a DB-backed GlobalLock keyed on (storeId, bucketName) around their whole body, so the two can never interleave for a given name. The existing per-account IAM lock is retained inside for credential and policy serialization.
S3 endpoint URLs are operator-supplied and accepted with or without a trailing slash, but two places appended a '/'-prefixed suffix to them directly: - createBucket persisted BucketVO.bucketURL as s3Url + "/" + bucketName, so an endpoint ending in '/' produced a broken '...//bucket' that both BucketResponse and the object store browser then used. - StorageManagerImpl.updateBucketUrls concatenated the new base URL with the retained '/bucket' suffix, so a trailing slash on the new pool URL rewrote every bucket URL to '...//bucket'. Added SeaweedFSObjectStoreUtil.stripTrailingSlashes and applied it in the driver, reused it in the metrics scrape which had its own inline loop, and normalized both bases in updateBucketUrls.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved issues affect persistence, cleanup, endpoint handling, and quota-update atomicity.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (5)
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:897
- The state is changed in memory, but a failed DAO update is ignored. On a later retry (for example, after accounting cleanup failed), the persisted row can still look live;
createUserwill then rebuild the policy and re-grant this ARN even though the remote bucket was deleted and its name may have been reused by another account. Propagate the update failure before refreshing the IAM policy.
_bucketDao.update(bvo.getId(), bvo);
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:580
- If this policy-refresh cleanup fails, the method still rethrows after only adding a suppressed exception. Because the caller assigns
bucketCreatedonly whencreateBucketreturns, it will skip its owndeleteBucketcleanup, remove the CloudStack row, and never retry this IAM revocation. A later account can reuse the bucket name while the old account policy still grants the ARN, allowing stale credentials to access the new tenant's bucket. Keep the row/operation retryable until policy removal succeeds or invoke an idempotent cleanup path that guarantees the refresh is retried.
logger.warn("Failed to revoke IAM policy for bucket {} after cleanup: {}", bucketName, policyEx.getMessage());
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:546
- This persists any path prefix from
s3UrlintoBucketURL, but the existing object-store browser derives its MinIO endpoint from only the host and port and drops that prefix (ui/src/components/view/ObjectStoreBrowser.vue:451-456). A SeaweedFS deployment behind a path such as/s3will work for server-side calls but browser listing/upload requests target the wrong endpoint. Either reject path-prefixed overrides or update the browser client construction to preserve the endpoint path.
bucketVO.setBucketURL(SeaweedFSObjectStoreUtil.stripTrailingSlashes(s3Url) + "/" + bucketName);
server/src/main/java/com/cloud/storage/StorageManagerImpl.java:4857
- This prefix check also matches unrelated endpoints that merely share the same text (for example,
http://s3:83330/...whenoldBaseishttp://s3:8333). The resulting suffix then corrupts that bucket's URL during an object-store update. Require the old base followed by the path separator before rewriting.
if (bucketUrl == null || !bucketUrl.startsWith(oldBase)) {
server/src/main/java/org/apache/cloudstack/storage/object/BucketApiServiceImpl.java:337
- The revised quota path can fail after the encryption/versioning/policy calls above have already changed the remote bucket—for example, the new reservation can be rejected for an over-limit request. This catch only wraps the exception, so the API can return an error while those remote settings remain changed and the BucketVO still has the old values. Roll back the earlier remote mutations or make the whole update atomic before reporting failure.
boolean bucketPersisted = updateBucketQuota(cmd, bucket, objectStore, objectStoreVO, bucketTO);
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Lite
| // trailing slash, which would persist a broken "...//bucket" URL | ||
| // that BucketResponse and the object store browser both use. | ||
| bucketVO.setBucketURL(SeaweedFSObjectStoreUtil.stripTrailingSlashes(s3Url) + "/" + bucketName); | ||
| _bucketDao.update(bucket.getId(), bucketVO); |
| } | ||
| String suffix = bucketUrl.substring(oldBase.length()); | ||
| bucket.setBucketURL(newBase + (suffix.startsWith("/") ? suffix : "/" + suffix)); | ||
| _bucketDao.update(bucket.getId(), bucket); |
|
Addressed Copilot review
Validated with:
|
Summary
Adds SeaweedFS as a first-class object storage provider in CloudStack, alongside the existing MinIO, Ceph RGW, and Cloudian HyperStore providers.
SeaweedFS exposes an S3-compatible API and an AWS IAM-compatible API, so this provider uses the AWS S3 and IAM Java SDKs — the same approach as the Cloudian HyperStore provider. No proprietary admin client is needed.
Key features
AmazonS3SDK (AWS SDK v1, same as Ceph/Cloudian)AmazonIdentityManagementSDK (same as Cloudian HyperStore)?seaweedfs-quotaextension (PUT /{bucket}?seaweedfs-quota), authenticated via SigV4 and authorized via thes3:PutBucketQuotaIAM permission. This requires SeaweedFS PR Multiple guest networks deployment issue #11279 (merged).ListObjectsV2(MVP; Prometheus or SOSAPIcapacity.xmlrecommended for production scale)Architecture
The plugin follows the Cloudian HyperStore pattern almost line for line:
s3Url,iamUrl,accesskey,secretkeyiamUrldefaults tos3Url(SeaweedFS registers its IAM API atPOST /on the same S3 endpoint)Quota management
SeaweedFS enforces bucket quota server-side (read-only flag when usage exceeds the limit). The configuration surface is a narrow S3 subresource —
PUT /{bucket}?seaweedfs-quota— authenticated via standard S3 SigV4 and authorized via dedicateds3:PutBucketQuota/s3:GetBucketQuotaIAM permissions. This avoids exposing the broad SeaweedFS admin API to CloudStack. The plugin signs the request withAWSS3V4Signerand sends it viajava.net.http.HttpClient(the AWS S3 SDK doesn't natively support custom subresources).Comparison with MinIO and Ceph
MinioClientAmazonS3AmazonS3AmazonS3MinioAdminClientRgwAdminAmazonIdentityManagementAmazonIdentityManagementMinioAdminClientRgwAdminMinioAdminClientRgwAdminListObjectsV2Files
New module under
plugins/storage/object/seaweedfs/:pom.xml— Maven moduleSeaweedFSObjectStoreProviderImpl.java— Spring provider registrationSeaweedFSObjectStoreLifeCycleImpl.java— Pool add/health-check, URL validationSeaweedFSObjectStoreDriverImpl.java— Bucket + user ops via S3 + IAM SDKSeaweedFSObjectStoreUtil.java— S3 + IAM client builders, constants, SigV4 quota requestSeaweedFS dependency
Requires SeaweedFS with the
?seaweedfs-quotaS3 extension (PR seaweedfs/seaweedfs#11279, merged). Without it, quota operations will fail with 404; all other operations (bucket CRUD, user provisioning, usage) work with any recent SeaweedFS release.Test plan
mvn -pl plugins/storage/object/seaweedfs test(18 tests, 0 failures)addObjectStoragePoolwiths3Url,accesskey,secretkeyweed shells3.bucket.list